{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Configurations for Colab"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:40:19.878722700Z",
     "start_time": "2023-09-06T05:40:19.835961300Z"
    }
   },
   "outputs": [],
   "source": [
    "import sys\n",
    "IN_COLAB = \"google.colab\" in sys.modules\n",
    "\n",
    "if IN_COLAB:\n",
    "    !apt install python-opengl\n",
    "    !apt install ffmpeg\n",
    "    !apt install xvfb\n",
    "    !pip install PyVirtualDisplay==3.0\n",
    "    !pip install gymnasium==0.28.1\n",
    "    from pyvirtualdisplay import Display\n",
    "    \n",
    "    # Start virtual display\n",
    "    dis = Display(visible=0, size=(400, 400))\n",
    "    dis.start()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 08. Rainbow\n",
    "\n",
    "[M. Hessel et al., \"Rainbow: Combining Improvements in Deep Reinforcement Learning.\" arXiv preprint arXiv:1710.02298, 2017.](https://arxiv.org/pdf/1710.02298.pdf)\n",
    "\n",
    "We will integrate all the following seven components into a single integrated agent, which is called Rainbow!\n",
    "\n",
    "1. DQN\n",
    "2. Double DQN\n",
    "3. Prioritized Experience Replay\n",
    "4. Dueling Network\n",
    "5. Noisy Network\n",
    "6. Categorical DQN\n",
    "7. N-step Learning\n",
    "\n",
    "This method shows an impressive performance on the Atari 2600 benchmark, both in terms of data efficiency and final performance. \n",
    "\n",
    "![rainbow](https://user-images.githubusercontent.com/14961526/60591412-61748100-9dd9-11e9-84fb-076c7a61fbab.png)\n",
    "\n",
    "However, the integration is not so simple because some of components are not independent each other, so we will look into a number of points that people especailly feel confused.\n",
    "\n",
    "1. Noisy Network <-> Dueling Network\n",
    "2. Dueling Network <-> Categorical DQN\n",
    "3. Categorical DQN <-> Double DQN"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:40:19.915722600Z",
     "start_time": "2023-09-06T05:40:19.846960800Z"
    }
   },
   "outputs": [],
   "source": [
    "import math\n",
    "import os\n",
    "import random\n",
    "from collections import deque\n",
    "from typing import Deque, Dict, List, Tuple\n",
    "\n",
    "import gymnasium as gym\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "import torch.optim as optim\n",
    "from IPython.display import clear_output\n",
    "from torch.nn.utils import clip_grad_norm_\n",
    "\n",
    "# download segment tree module\n",
    "if IN_COLAB:\n",
    "    !wget https://raw.githubusercontent.com/curt-park/rainbow-is-all-you-need/master/segment_tree.py\n",
    "\n",
    "from segment_tree import MinSegmentTree, SumSegmentTree"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Replay buffer\n",
    "\n",
    "Same as the basic N-step buffer. \n",
    "\n",
    "(Please see *01.dqn.ipynb*, *07.n_step_learning.ipynb* for detailed description about the basic (n-step) replay buffer.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:40:19.915722600Z",
     "start_time": "2023-09-06T05:40:19.866725600Z"
    }
   },
   "outputs": [],
   "source": [
    "class ReplayBuffer:\n",
    "    \"\"\"A simple numpy replay buffer.\"\"\"\n",
    "\n",
    "    def __init__(\n",
    "        self, \n",
    "        obs_dim: int, \n",
    "        size: int, \n",
    "        batch_size: int = 32, \n",
    "        n_step: int = 1, \n",
    "        gamma: float = 0.99\n",
    "    ):\n",
    "        self.obs_buf = np.zeros([size, obs_dim], dtype=np.float32)\n",
    "        self.next_obs_buf = np.zeros([size, obs_dim], dtype=np.float32)\n",
    "        self.acts_buf = np.zeros([size], dtype=np.float32)\n",
    "        self.rews_buf = np.zeros([size], dtype=np.float32)\n",
    "        self.done_buf = np.zeros(size, dtype=np.float32)\n",
    "        self.max_size, self.batch_size = size, batch_size\n",
    "        self.ptr, self.size, = 0, 0\n",
    "        \n",
    "        # for N-step Learning\n",
    "        self.n_step_buffer = deque(maxlen=n_step)\n",
    "        self.n_step = n_step\n",
    "        self.gamma = gamma\n",
    "\n",
    "    def store(\n",
    "        self, \n",
    "        obs: np.ndarray, \n",
    "        act: np.ndarray, \n",
    "        rew: float, \n",
    "        next_obs: np.ndarray, \n",
    "        done: bool,\n",
    "    ) -> Tuple[np.ndarray, np.ndarray, float, np.ndarray, bool]:\n",
    "        transition = (obs, act, rew, next_obs, done)\n",
    "        self.n_step_buffer.append(transition)\n",
    "\n",
    "        # single step transition is not ready\n",
    "        if len(self.n_step_buffer) < self.n_step:\n",
    "            return ()\n",
    "        \n",
    "        # make a n-step transition\n",
    "        rew, next_obs, done = self._get_n_step_info(\n",
    "            self.n_step_buffer, self.gamma\n",
    "        )\n",
    "        obs, act = self.n_step_buffer[0][:2]\n",
    "        \n",
    "        self.obs_buf[self.ptr] = obs\n",
    "        self.next_obs_buf[self.ptr] = next_obs\n",
    "        self.acts_buf[self.ptr] = act\n",
    "        self.rews_buf[self.ptr] = rew\n",
    "        self.done_buf[self.ptr] = done\n",
    "        self.ptr = (self.ptr + 1) % self.max_size\n",
    "        self.size = min(self.size + 1, self.max_size)\n",
    "        \n",
    "        return self.n_step_buffer[0]\n",
    "\n",
    "    def sample_batch(self) -> Dict[str, np.ndarray]:\n",
    "        idxs = np.random.choice(self.size, size=self.batch_size, replace=False)\n",
    "\n",
    "        return dict(\n",
    "            obs=self.obs_buf[idxs],\n",
    "            next_obs=self.next_obs_buf[idxs],\n",
    "            acts=self.acts_buf[idxs],\n",
    "            rews=self.rews_buf[idxs],\n",
    "            done=self.done_buf[idxs],\n",
    "            # for N-step Learning\n",
    "            indices=idxs,\n",
    "        )\n",
    "    \n",
    "    def sample_batch_from_idxs(\n",
    "        self, idxs: np.ndarray\n",
    "    ) -> Dict[str, np.ndarray]:\n",
    "        # for N-step Learning\n",
    "        return dict(\n",
    "            obs=self.obs_buf[idxs],\n",
    "            next_obs=self.next_obs_buf[idxs],\n",
    "            acts=self.acts_buf[idxs],\n",
    "            rews=self.rews_buf[idxs],\n",
    "            done=self.done_buf[idxs],\n",
    "        )\n",
    "    \n",
    "    def _get_n_step_info(\n",
    "        self, n_step_buffer: Deque, gamma: float\n",
    "    ) -> Tuple[np.int64, np.ndarray, bool]:\n",
    "        \"\"\"Return n step rew, next_obs, and done.\"\"\"\n",
    "        # info of the last transition\n",
    "        rew, next_obs, done = n_step_buffer[-1][-3:]\n",
    "\n",
    "        for transition in reversed(list(n_step_buffer)[:-1]):\n",
    "            r, n_o, d = transition[-3:]\n",
    "\n",
    "            rew = r + gamma * rew * (1 - d)\n",
    "            next_obs, done = (n_o, d) if d else (next_obs, done)\n",
    "\n",
    "        return rew, next_obs, done\n",
    "\n",
    "    def __len__(self) -> int:\n",
    "        return self.size"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Prioritized replay Buffer\n",
    "\n",
    "`store` method returns boolean in order to inform if a N-step transition has been generated.\n",
    "\n",
    "(Please see *02.per.ipynb* for detailed description about PER.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:40:19.916773800Z",
     "start_time": "2023-09-06T05:40:19.875722200Z"
    }
   },
   "outputs": [],
   "source": [
    "class PrioritizedReplayBuffer(ReplayBuffer):\n",
    "    \"\"\"Prioritized Replay buffer.\n",
    "    \n",
    "    Attributes:\n",
    "        max_priority (float): max priority\n",
    "        tree_ptr (int): next index of tree\n",
    "        alpha (float): alpha parameter for prioritized replay buffer\n",
    "        sum_tree (SumSegmentTree): sum tree for prior\n",
    "        min_tree (MinSegmentTree): min tree for min prior to get max weight\n",
    "        \n",
    "    \"\"\"\n",
    "    \n",
    "    def __init__(\n",
    "        self, \n",
    "        obs_dim: int, \n",
    "        size: int, \n",
    "        batch_size: int = 32, \n",
    "        alpha: float = 0.6,\n",
    "        n_step: int = 1, \n",
    "        gamma: float = 0.99,\n",
    "    ):\n",
    "        \"\"\"Initialization.\"\"\"\n",
    "        assert alpha >= 0\n",
    "        \n",
    "        super(PrioritizedReplayBuffer, self).__init__(\n",
    "            obs_dim, size, batch_size, n_step, gamma\n",
    "        )\n",
    "        self.max_priority, self.tree_ptr = 1.0, 0\n",
    "        self.alpha = alpha\n",
    "        \n",
    "        # capacity must be positive and a power of 2.\n",
    "        tree_capacity = 1\n",
    "        while tree_capacity < self.max_size:\n",
    "            tree_capacity *= 2\n",
    "\n",
    "        self.sum_tree = SumSegmentTree(tree_capacity)\n",
    "        self.min_tree = MinSegmentTree(tree_capacity)\n",
    "        \n",
    "    def store(\n",
    "        self, \n",
    "        obs: np.ndarray, \n",
    "        act: int, \n",
    "        rew: float, \n",
    "        next_obs: np.ndarray, \n",
    "        done: bool,\n",
    "    ) -> Tuple[np.ndarray, np.ndarray, float, np.ndarray, bool]:\n",
    "        \"\"\"Store experience and priority.\"\"\"\n",
    "        transition = super().store(obs, act, rew, next_obs, done)\n",
    "        \n",
    "        if transition:\n",
    "            self.sum_tree[self.tree_ptr] = self.max_priority ** self.alpha\n",
    "            self.min_tree[self.tree_ptr] = self.max_priority ** self.alpha\n",
    "            self.tree_ptr = (self.tree_ptr + 1) % self.max_size\n",
    "        \n",
    "        return transition\n",
    "\n",
    "    def sample_batch(self, beta: float = 0.4) -> Dict[str, np.ndarray]:\n",
    "        \"\"\"Sample a batch of experiences.\"\"\"\n",
    "        assert len(self) >= self.batch_size\n",
    "        assert beta > 0\n",
    "        \n",
    "        indices = self._sample_proportional()\n",
    "        \n",
    "        obs = self.obs_buf[indices]\n",
    "        next_obs = self.next_obs_buf[indices]\n",
    "        acts = self.acts_buf[indices]\n",
    "        rews = self.rews_buf[indices]\n",
    "        done = self.done_buf[indices]\n",
    "        weights = np.array([self._calculate_weight(i, beta) for i in indices])\n",
    "        \n",
    "        return dict(\n",
    "            obs=obs,\n",
    "            next_obs=next_obs,\n",
    "            acts=acts,\n",
    "            rews=rews,\n",
    "            done=done,\n",
    "            weights=weights,\n",
    "            indices=indices,\n",
    "        )\n",
    "        \n",
    "    def update_priorities(self, indices: List[int], priorities: np.ndarray):\n",
    "        \"\"\"Update priorities of sampled transitions.\"\"\"\n",
    "        assert len(indices) == len(priorities)\n",
    "\n",
    "        for idx, priority in zip(indices, priorities):\n",
    "            assert priority > 0\n",
    "            assert 0 <= idx < len(self)\n",
    "\n",
    "            self.sum_tree[idx] = priority ** self.alpha\n",
    "            self.min_tree[idx] = priority ** self.alpha\n",
    "\n",
    "            self.max_priority = max(self.max_priority, priority)\n",
    "            \n",
    "    def _sample_proportional(self) -> List[int]:\n",
    "        \"\"\"Sample indices based on proportions.\"\"\"\n",
    "        indices = []\n",
    "        p_total = self.sum_tree.sum(0, len(self) - 1)\n",
    "        segment = p_total / self.batch_size\n",
    "        \n",
    "        for i in range(self.batch_size):\n",
    "            a = segment * i\n",
    "            b = segment * (i + 1)\n",
    "            upperbound = random.uniform(a, b)\n",
    "            idx = self.sum_tree.retrieve(upperbound)\n",
    "            indices.append(idx)\n",
    "            \n",
    "        return indices\n",
    "    \n",
    "    def _calculate_weight(self, idx: int, beta: float):\n",
    "        \"\"\"Calculate the weight of the experience at idx.\"\"\"\n",
    "        # get max weight\n",
    "        p_min = self.min_tree.min() / self.sum_tree.sum()\n",
    "        max_weight = (p_min * len(self)) ** (-beta)\n",
    "        \n",
    "        # calculate weights\n",
    "        p_sample = self.sum_tree[idx] / self.sum_tree.sum()\n",
    "        weight = (p_sample * len(self)) ** (-beta)\n",
    "        weight = weight / max_weight\n",
    "        \n",
    "        return weight"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Noisy Layer\n",
    "\n",
    "Please see *05.noisy_net.ipynb* for detailed description.\n",
    "\n",
    "**References:**\n",
    "\n",
    "- https://github.com/higgsfield/RL-Adventure/blob/master/5.noisy%20dqn.ipynb\n",
    "- https://github.com/Kaixhin/Rainbow/blob/master/model.py"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:40:19.916773800Z",
     "start_time": "2023-09-06T05:40:19.895723Z"
    }
   },
   "outputs": [],
   "source": [
    "class NoisyLinear(nn.Module):\n",
    "    \"\"\"Noisy linear module for NoisyNet.\n",
    "    \n",
    "    \n",
    "        \n",
    "    Attributes:\n",
    "        in_features (int): input size of linear module\n",
    "        out_features (int): output size of linear module\n",
    "        std_init (float): initial std value\n",
    "        weight_mu (nn.Parameter): mean value weight parameter\n",
    "        weight_sigma (nn.Parameter): std value weight parameter\n",
    "        bias_mu (nn.Parameter): mean value bias parameter\n",
    "        bias_sigma (nn.Parameter): std value bias parameter\n",
    "        \n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(\n",
    "        self, \n",
    "        in_features: int, \n",
    "        out_features: int, \n",
    "        std_init: float = 0.5,\n",
    "    ):\n",
    "        \"\"\"Initialization.\"\"\"\n",
    "        super(NoisyLinear, self).__init__()\n",
    "        \n",
    "        self.in_features = in_features\n",
    "        self.out_features = out_features\n",
    "        self.std_init = std_init\n",
    "\n",
    "        self.weight_mu = nn.Parameter(torch.Tensor(out_features, in_features))\n",
    "        self.weight_sigma = nn.Parameter(\n",
    "            torch.Tensor(out_features, in_features)\n",
    "        )\n",
    "        self.register_buffer(\n",
    "            \"weight_epsilon\", torch.Tensor(out_features, in_features)\n",
    "        )\n",
    "\n",
    "        self.bias_mu = nn.Parameter(torch.Tensor(out_features))\n",
    "        self.bias_sigma = nn.Parameter(torch.Tensor(out_features))\n",
    "        self.register_buffer(\"bias_epsilon\", torch.Tensor(out_features))\n",
    "\n",
    "        self.reset_parameters()\n",
    "        self.reset_noise()\n",
    "\n",
    "    def reset_parameters(self):\n",
    "        \"\"\"Reset trainable network parameters (factorized gaussian noise).\"\"\"\n",
    "        mu_range = 1 / math.sqrt(self.in_features)\n",
    "        self.weight_mu.data.uniform_(-mu_range, mu_range)\n",
    "        self.weight_sigma.data.fill_(\n",
    "            self.std_init / math.sqrt(self.in_features)\n",
    "        )\n",
    "        self.bias_mu.data.uniform_(-mu_range, mu_range)\n",
    "        self.bias_sigma.data.fill_(\n",
    "            self.std_init / math.sqrt(self.out_features)\n",
    "        )\n",
    "\n",
    "    def reset_noise(self):\n",
    "        \"\"\"Make new noise.\"\"\"\n",
    "        epsilon_in = self.scale_noise(self.in_features)\n",
    "        epsilon_out = self.scale_noise(self.out_features)\n",
    "\n",
    "        # outer product\n",
    "        self.weight_epsilon.copy_(epsilon_out.ger(epsilon_in))\n",
    "        self.bias_epsilon.copy_(epsilon_out)\n",
    "\n",
    "    def forward(self, x: torch.Tensor) -> torch.Tensor:\n",
    "        \"\"\"Forward method implementation.\n",
    "        \n",
    "        We don't use separate statements on train / eval mode.\n",
    "        It doesn't show remarkable difference of performance.\n",
    "        \"\"\"\n",
    "        return F.linear(\n",
    "            x,\n",
    "            self.weight_mu + self.weight_sigma * self.weight_epsilon,\n",
    "            self.bias_mu + self.bias_sigma * self.bias_epsilon,\n",
    "        )\n",
    "    \n",
    "    @staticmethod\n",
    "    def scale_noise(size: int) -> torch.Tensor:\n",
    "        \"\"\"Set scale to make noise (factorized gaussian noise).\"\"\"\n",
    "        x = torch.randn(size)\n",
    "\n",
    "        return x.sign().mul(x.abs().sqrt())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## NoisyNet + DuelingNet + Categorical DQN\n",
    "\n",
    "#### NoisyNet + DuelingNet\n",
    "\n",
    "NoisyLinear is employed for the last two layers of advantage and value layers. The noise should be reset at evey update step.\n",
    "\n",
    "#### DuelingNet + Categorical DQN\n",
    "\n",
    "The dueling network architecture is adapted for use with return distributions. The network has a shared representation, which is then fed into a value stream with atom_size outputs, and into an advantage stream with atom_size × out_dim outputs. For each atom, the value and advantage streams are aggregated, as in dueling DQN, and then passed through a softmax layer to obtain the normalized parametric distributions used to estimate the returns’ distributions.\n",
    "\n",
    "```\n",
    "        advantage = self.advantage_layer(adv_hid).view(-1, self.out_dim, self.atom_size)\n",
    "        value = self.value_layer(val_hid).view(-1, 1, self.atom_size)\n",
    "        q_atoms = value + advantage - advantage.mean(dim=1, keepdim=True)\n",
    "        \n",
    "        dist = F.softmax(q_atoms, dim=-1)\n",
    "```\n",
    "\n",
    "(Please see *04.dueling.ipynb*, *05.noisy_net.ipynb*, *06.categorical_dqn.ipynb* for detailed description of each component's network architecture.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:40:19.930722100Z",
     "start_time": "2023-09-06T05:40:19.912722500Z"
    }
   },
   "outputs": [],
   "source": [
    "class Network(nn.Module):\n",
    "    def __init__(\n",
    "        self, \n",
    "        in_dim: int, \n",
    "        out_dim: int, \n",
    "        atom_size: int, \n",
    "        support: torch.Tensor\n",
    "    ):\n",
    "        \"\"\"Initialization.\"\"\"\n",
    "        super(Network, self).__init__()\n",
    "        \n",
    "        self.support = support\n",
    "        self.out_dim = out_dim\n",
    "        self.atom_size = atom_size\n",
    "\n",
    "        # set common feature layer\n",
    "        self.feature_layer = nn.Sequential(\n",
    "            nn.Linear(in_dim, 128), \n",
    "            nn.ReLU(),\n",
    "        )\n",
    "        \n",
    "        # set advantage layer\n",
    "        self.advantage_hidden_layer = NoisyLinear(128, 128)\n",
    "        self.advantage_layer = NoisyLinear(128, out_dim * atom_size)\n",
    "\n",
    "        # set value layer\n",
    "        self.value_hidden_layer = NoisyLinear(128, 128)\n",
    "        self.value_layer = NoisyLinear(128, atom_size)\n",
    "\n",
    "    def forward(self, x: torch.Tensor) -> torch.Tensor:\n",
    "        \"\"\"Forward method implementation.\"\"\"\n",
    "        dist = self.dist(x)\n",
    "        q = torch.sum(dist * self.support, dim=2)\n",
    "        \n",
    "        return q\n",
    "    \n",
    "    def dist(self, x: torch.Tensor) -> torch.Tensor:\n",
    "        \"\"\"Get distribution for atoms.\"\"\"\n",
    "        feature = self.feature_layer(x)\n",
    "        adv_hid = F.relu(self.advantage_hidden_layer(feature))\n",
    "        val_hid = F.relu(self.value_hidden_layer(feature))\n",
    "        \n",
    "        advantage = self.advantage_layer(adv_hid).view(\n",
    "            -1, self.out_dim, self.atom_size\n",
    "        )\n",
    "        value = self.value_layer(val_hid).view(-1, 1, self.atom_size)\n",
    "        q_atoms = value + advantage - advantage.mean(dim=1, keepdim=True)\n",
    "        \n",
    "        dist = F.softmax(q_atoms, dim=-1)\n",
    "        dist = dist.clamp(min=1e-3)  # for avoiding nans\n",
    "        \n",
    "        return dist\n",
    "    \n",
    "    def reset_noise(self):\n",
    "        \"\"\"Reset all noisy layers.\"\"\"\n",
    "        self.advantage_hidden_layer.reset_noise()\n",
    "        self.advantage_layer.reset_noise()\n",
    "        self.value_hidden_layer.reset_noise()\n",
    "        self.value_layer.reset_noise()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Rainbow Agent\n",
    "\n",
    "Here is a summary of DQNAgent class.\n",
    "\n",
    "| Method           | Note                                                 |\n",
    "| ---              | ---                                                  |\n",
    "|select_action     | select an action from the input state.               |\n",
    "|step              | take an action and return the response of the env.   |\n",
    "|compute_dqn_loss  | return dqn loss.                                     |\n",
    "|update_model      | update the model by gradient descent.                |\n",
    "|target_hard_update| hard update from the local model to the target model.|\n",
    "|train             | train the agent during num_frames.                   |\n",
    "|test              | test the agent (1 episode).                          |\n",
    "|plot              | plot the training progresses.                        |\n",
    "\n",
    "#### Categorical DQN + Double DQN\n",
    "\n",
    "The idea of Double Q-learning is to reduce overestimations by decomposing the max operation in the target into action selection and action evaluation. Here, we use `self.dqn` instead of `self.dqn_target` to obtain the target actions.\n",
    "\n",
    "```\n",
    "        # Categorical DQN + Double DQN\n",
    "        # target_dqn is used when we don't employ double DQN\n",
    "        next_action = self.dqn(next_state).argmax(1)\n",
    "        next_dist = self.dqn_target.dist(next_state)\n",
    "        next_dist = next_dist[range(self.batch_size), next_action]\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:40:19.966722400Z",
     "start_time": "2023-09-06T05:40:19.922729400Z"
    }
   },
   "outputs": [],
   "source": [
    "class DQNAgent:\n",
    "    \"\"\"DQN Agent interacting with environment.\n",
    "    \n",
    "    Attribute:\n",
    "        env (gym.Env): openAI Gym environment\n",
    "        memory (PrioritizedReplayBuffer): replay memory to store transitions\n",
    "        batch_size (int): batch size for sampling\n",
    "        target_update (int): period for target model's hard update\n",
    "        gamma (float): discount factor\n",
    "        dqn (Network): model to train and select actions\n",
    "        dqn_target (Network): target model to update\n",
    "        optimizer (torch.optim): optimizer for training dqn\n",
    "        transition (list): transition information including \n",
    "                           state, action, reward, next_state, done\n",
    "        v_min (float): min value of support\n",
    "        v_max (float): max value of support\n",
    "        atom_size (int): the unit number of support\n",
    "        support (torch.Tensor): support for categorical dqn\n",
    "        use_n_step (bool): whether to use n_step memory\n",
    "        n_step (int): step number to calculate n-step td error\n",
    "        memory_n (ReplayBuffer): n-step replay buffer\n",
    "    \"\"\"\n",
    "\n",
    "    def __init__(\n",
    "        self, \n",
    "        env: gym.Env,\n",
    "        memory_size: int,\n",
    "        batch_size: int,\n",
    "        target_update: int,\n",
    "        seed: int,\n",
    "        gamma: float = 0.99,\n",
    "        # PER parameters\n",
    "        alpha: float = 0.2,\n",
    "        beta: float = 0.6,\n",
    "        prior_eps: float = 1e-6,\n",
    "        # Categorical DQN parameters\n",
    "        v_min: float = 0.0,\n",
    "        v_max: float = 200.0,\n",
    "        atom_size: int = 51,\n",
    "        # N-step Learning\n",
    "        n_step: int = 3,\n",
    "    ):\n",
    "        \"\"\"Initialization.\n",
    "        \n",
    "        Args:\n",
    "            env (gym.Env): openAI Gym environment\n",
    "            memory_size (int): length of memory\n",
    "            batch_size (int): batch size for sampling\n",
    "            target_update (int): period for target model's hard update\n",
    "            lr (float): learning rate\n",
    "            gamma (float): discount factor\n",
    "            alpha (float): determines how much prioritization is used\n",
    "            beta (float): determines how much importance sampling is used\n",
    "            prior_eps (float): guarantees every transition can be sampled\n",
    "            v_min (float): min value of support\n",
    "            v_max (float): max value of support\n",
    "            atom_size (int): the unit number of support\n",
    "            n_step (int): step number to calculate n-step td error\n",
    "        \"\"\"\n",
    "        obs_dim = env.observation_space.shape[0]\n",
    "        action_dim = env.action_space.n\n",
    "        \n",
    "        self.env = env\n",
    "        self.batch_size = batch_size\n",
    "        self.target_update = target_update\n",
    "        self.seed = seed\n",
    "        self.gamma = gamma\n",
    "        # NoisyNet: All attributes related to epsilon are removed\n",
    "        \n",
    "        # device: cpu / gpu\n",
    "        self.device = torch.device(\n",
    "            \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
    "        )\n",
    "        print(self.device)\n",
    "        \n",
    "        # PER\n",
    "        # memory for 1-step Learning\n",
    "        self.beta = beta\n",
    "        self.prior_eps = prior_eps\n",
    "        self.memory = PrioritizedReplayBuffer(\n",
    "            obs_dim, memory_size, batch_size, alpha=alpha\n",
    "        )\n",
    "        \n",
    "        # memory for N-step Learning\n",
    "        self.use_n_step = True if n_step > 1 else False\n",
    "        if self.use_n_step:\n",
    "            self.n_step = n_step\n",
    "            self.memory_n = ReplayBuffer(\n",
    "                obs_dim, memory_size, batch_size, n_step=n_step, gamma=gamma\n",
    "            )\n",
    "            \n",
    "        # Categorical DQN parameters\n",
    "        self.v_min = v_min\n",
    "        self.v_max = v_max\n",
    "        self.atom_size = atom_size\n",
    "        self.support = torch.linspace(\n",
    "            self.v_min, self.v_max, self.atom_size\n",
    "        ).to(self.device)\n",
    "\n",
    "        # networks: dqn, dqn_target\n",
    "        self.dqn = Network(\n",
    "            obs_dim, action_dim, self.atom_size, self.support\n",
    "        ).to(self.device)\n",
    "        self.dqn_target = Network(\n",
    "            obs_dim, action_dim, self.atom_size, self.support\n",
    "        ).to(self.device)\n",
    "        self.dqn_target.load_state_dict(self.dqn.state_dict())\n",
    "        self.dqn_target.eval()\n",
    "        \n",
    "        # optimizer\n",
    "        self.optimizer = optim.Adam(self.dqn.parameters())\n",
    "\n",
    "        # transition to store in memory\n",
    "        self.transition = list()\n",
    "        \n",
    "        # mode: train / test\n",
    "        self.is_test = False\n",
    "\n",
    "    def select_action(self, state: np.ndarray) -> np.ndarray:\n",
    "        \"\"\"Select an action from the input state.\"\"\"\n",
    "        # NoisyNet: no epsilon greedy action selection\n",
    "        selected_action = self.dqn(\n",
    "            torch.FloatTensor(state).to(self.device)\n",
    "        ).argmax()\n",
    "        selected_action = selected_action.detach().cpu().numpy()\n",
    "        \n",
    "        if not self.is_test:\n",
    "            self.transition = [state, selected_action]\n",
    "        \n",
    "        return selected_action\n",
    "\n",
    "    def step(self, action: np.ndarray) -> Tuple[np.ndarray, np.float64, bool]:\n",
    "        \"\"\"Take an action and return the response of the env.\"\"\"\n",
    "        next_state, reward, terminated, truncated, _ = self.env.step(action)\n",
    "        done = terminated or truncated\n",
    "        \n",
    "        if not self.is_test:\n",
    "            self.transition += [reward, next_state, done]\n",
    "            \n",
    "            # N-step transition\n",
    "            if self.use_n_step:\n",
    "                one_step_transition = self.memory_n.store(*self.transition)\n",
    "            # 1-step transition\n",
    "            else:\n",
    "                one_step_transition = self.transition\n",
    "\n",
    "            # add a single step transition\n",
    "            if one_step_transition:\n",
    "                self.memory.store(*one_step_transition)\n",
    "    \n",
    "        return next_state, reward, done\n",
    "\n",
    "    def update_model(self) -> torch.Tensor:\n",
    "        \"\"\"Update the model by gradient descent.\"\"\"\n",
    "        # PER needs beta to calculate weights\n",
    "        samples = self.memory.sample_batch(self.beta)\n",
    "        weights = torch.FloatTensor(\n",
    "            samples[\"weights\"].reshape(-1, 1)\n",
    "        ).to(self.device)\n",
    "        indices = samples[\"indices\"]\n",
    "        \n",
    "        # 1-step Learning loss\n",
    "        elementwise_loss = self._compute_dqn_loss(samples, self.gamma)\n",
    "        \n",
    "        # PER: importance sampling before average\n",
    "        loss = torch.mean(elementwise_loss * weights)\n",
    "        \n",
    "        # N-step Learning loss\n",
    "        # we are gonna combine 1-step loss and n-step loss so as to\n",
    "        # prevent high-variance. The original rainbow employs n-step loss only.\n",
    "        if self.use_n_step:\n",
    "            gamma = self.gamma ** self.n_step\n",
    "            samples = self.memory_n.sample_batch_from_idxs(indices)\n",
    "            elementwise_loss_n_loss = self._compute_dqn_loss(samples, gamma)\n",
    "            elementwise_loss += elementwise_loss_n_loss\n",
    "            \n",
    "            # PER: importance sampling before average\n",
    "            loss = torch.mean(elementwise_loss * weights)\n",
    "\n",
    "        self.optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        clip_grad_norm_(self.dqn.parameters(), 10.0)\n",
    "        self.optimizer.step()\n",
    "        \n",
    "        # PER: update priorities\n",
    "        loss_for_prior = elementwise_loss.detach().cpu().numpy()\n",
    "        new_priorities = loss_for_prior + self.prior_eps\n",
    "        self.memory.update_priorities(indices, new_priorities)\n",
    "        \n",
    "        # NoisyNet: reset noise\n",
    "        self.dqn.reset_noise()\n",
    "        self.dqn_target.reset_noise()\n",
    "\n",
    "        return loss.item()\n",
    "        \n",
    "    def train(self, num_frames: int, plotting_interval: int = 200):\n",
    "        \"\"\"Train the agent.\"\"\"\n",
    "        self.is_test = False\n",
    "        \n",
    "        state, _ = self.env.reset(seed=self.seed)\n",
    "        update_cnt = 0\n",
    "        losses = []\n",
    "        scores = []\n",
    "        score = 0\n",
    "\n",
    "        for frame_idx in range(1, num_frames + 1):\n",
    "            action = self.select_action(state)\n",
    "            next_state, reward, done = self.step(action)\n",
    "\n",
    "            state = next_state\n",
    "            score += reward\n",
    "            \n",
    "            # NoisyNet: removed decrease of epsilon\n",
    "            \n",
    "            # PER: increase beta\n",
    "            fraction = min(frame_idx / num_frames, 1.0)\n",
    "            self.beta = self.beta + fraction * (1.0 - self.beta)\n",
    "\n",
    "            # if episode ends\n",
    "            if done:\n",
    "                state, _ = self.env.reset(seed=self.seed)\n",
    "                scores.append(score)\n",
    "                score = 0\n",
    "\n",
    "            # if training is ready\n",
    "            if len(self.memory) >= self.batch_size:\n",
    "                loss = self.update_model()\n",
    "                losses.append(loss)\n",
    "                update_cnt += 1\n",
    "                \n",
    "                # if hard update is needed\n",
    "                if update_cnt % self.target_update == 0:\n",
    "                    self._target_hard_update()\n",
    "\n",
    "            # plotting\n",
    "            if frame_idx % plotting_interval == 0:\n",
    "                self._plot(frame_idx, scores, losses)\n",
    "                \n",
    "        self.env.close()\n",
    "                \n",
    "    def test(self, video_folder: str) -> None:\n",
    "        \"\"\"Test the agent.\"\"\"\n",
    "        self.is_test = True\n",
    "        \n",
    "        # for recording a video\n",
    "        naive_env = self.env\n",
    "        self.env = gym.wrappers.RecordVideo(self.env, video_folder=video_folder)\n",
    "        \n",
    "        state, _ = self.env.reset(seed=self.seed)\n",
    "        done = False\n",
    "        score = 0\n",
    "        \n",
    "        while not done:\n",
    "            action = self.select_action(state)\n",
    "            next_state, reward, done = self.step(action)\n",
    "\n",
    "            state = next_state\n",
    "            score += reward\n",
    "        \n",
    "        print(\"score: \", score)\n",
    "        self.env.close()\n",
    "        \n",
    "        # reset\n",
    "        self.env = naive_env\n",
    "\n",
    "    def _compute_dqn_loss(self, samples: Dict[str, np.ndarray], gamma: float) -> torch.Tensor:\n",
    "        \"\"\"Return categorical dqn loss.\"\"\"\n",
    "        device = self.device  # for shortening the following lines\n",
    "        state = torch.FloatTensor(samples[\"obs\"]).to(device)\n",
    "        next_state = torch.FloatTensor(samples[\"next_obs\"]).to(device)\n",
    "        action = torch.LongTensor(samples[\"acts\"]).to(device)\n",
    "        reward = torch.FloatTensor(samples[\"rews\"].reshape(-1, 1)).to(device)\n",
    "        done = torch.FloatTensor(samples[\"done\"].reshape(-1, 1)).to(device)\n",
    "        \n",
    "        # Categorical DQN algorithm\n",
    "        delta_z = float(self.v_max - self.v_min) / (self.atom_size - 1)\n",
    "\n",
    "        with torch.no_grad():\n",
    "            # Double DQN\n",
    "            next_action = self.dqn(next_state).argmax(1)\n",
    "            next_dist = self.dqn_target.dist(next_state)\n",
    "            next_dist = next_dist[range(self.batch_size), next_action]\n",
    "\n",
    "            t_z = reward + (1 - done) * gamma * self.support\n",
    "            t_z = t_z.clamp(min=self.v_min, max=self.v_max)\n",
    "            b = (t_z - self.v_min) / delta_z\n",
    "            l = b.floor().long()\n",
    "            u = b.ceil().long()\n",
    "\n",
    "            offset = (\n",
    "                torch.linspace(\n",
    "                    0, (self.batch_size - 1) * self.atom_size, self.batch_size\n",
    "                ).long()\n",
    "                .unsqueeze(1)\n",
    "                .expand(self.batch_size, self.atom_size)\n",
    "                .to(self.device)\n",
    "            )\n",
    "\n",
    "            proj_dist = torch.zeros(next_dist.size(), device=self.device)\n",
    "            proj_dist.view(-1).index_add_(\n",
    "                0, (l + offset).view(-1), (next_dist * (u.float() - b)).view(-1)\n",
    "            )\n",
    "            proj_dist.view(-1).index_add_(\n",
    "                0, (u + offset).view(-1), (next_dist * (b - l.float())).view(-1)\n",
    "            )\n",
    "\n",
    "        dist = self.dqn.dist(state)\n",
    "        log_p = torch.log(dist[range(self.batch_size), action])\n",
    "        elementwise_loss = -(proj_dist * log_p).sum(1)\n",
    "\n",
    "        return elementwise_loss\n",
    "\n",
    "    def _target_hard_update(self):\n",
    "        \"\"\"Hard update: target <- local.\"\"\"\n",
    "        self.dqn_target.load_state_dict(self.dqn.state_dict())\n",
    "                \n",
    "    def _plot(\n",
    "        self, \n",
    "        frame_idx: int, \n",
    "        scores: List[float], \n",
    "        losses: List[float],\n",
    "    ):\n",
    "        \"\"\"Plot the training progresses.\"\"\"\n",
    "        clear_output(True)\n",
    "        plt.figure(figsize=(20, 5))\n",
    "        plt.subplot(131)\n",
    "        plt.title('frame %s. score: %s' % (frame_idx, np.mean(scores[-10:])))\n",
    "        plt.plot(scores)\n",
    "        plt.subplot(132)\n",
    "        plt.title('loss')\n",
    "        plt.plot(losses)\n",
    "        plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Environment\n",
    "\n",
    "You can see the [code](https://github.com/Farama-Foundation/Gymnasium/blob/main/gymnasium/envs/classic_control/cartpole.py) and [configurations](https://github.com/Farama-Foundation/Gymnasium/blob/main/gymnasium/envs/classic_control/cartpole.py#L91) of CartPole-v1 from Farama Gymnasium's repository."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:40:19.974723500Z",
     "start_time": "2023-09-06T05:40:19.955722700Z"
    }
   },
   "outputs": [],
   "source": [
    "# environment\n",
    "env = gym.make(\"CartPole-v1\", max_episode_steps=200, render_mode=\"rgb_array\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Set random seed"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:40:19.992723500Z",
     "start_time": "2023-09-06T05:40:19.970766500Z"
    }
   },
   "outputs": [],
   "source": [
    "seed = 777\n",
    "\n",
    "def seed_torch(seed):\n",
    "    torch.manual_seed(seed)\n",
    "    if torch.backends.cudnn.enabled:\n",
    "        torch.cuda.manual_seed(seed)\n",
    "        torch.backends.cudnn.benchmark = False\n",
    "        torch.backends.cudnn.deterministic = True\n",
    "\n",
    "np.random.seed(seed)\n",
    "random.seed(seed)\n",
    "seed_torch(seed)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Initialize"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:40:20.050722200Z",
     "start_time": "2023-09-06T05:40:19.987723600Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "cuda\n"
     ]
    }
   ],
   "source": [
    "# parameters\n",
    "num_frames = 10000\n",
    "memory_size = 10000\n",
    "batch_size = 128\n",
    "target_update = 100\n",
    "\n",
    "# train\n",
    "agent = DQNAgent(env, memory_size, batch_size, target_update, seed)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Train"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": "<Figure size 2000x500 with 2 Axes>",
      "image/png": "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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "agent.train(num_frames)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Test\n",
    "\n",
    "Run the trained agent (1 episode)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:42:58.038908800Z",
     "start_time": "2023-09-06T05:42:56.859901700Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\18213\\anaconda3\\envs\\Rainbow\\lib\\site-packages\\gymnasium\\wrappers\\record_video.py:87: UserWarning: \u001B[33mWARN: Overwriting existing videos at C:\\Users\\18213\\Source\\Rainbow Reading Notes\\videos\\rainbow folder (try specifying a different `video_folder` for the `RecordVideo` wrapper if this is not desired)\u001B[0m\n",
      "  logger.warn(\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Moviepy - Building video C:\\Users\\18213\\Source\\Rainbow Reading Notes\\videos\\rainbow\\rl-video-episode-0.mp4.\n",
      "Moviepy - Writing video C:\\Users\\18213\\Source\\Rainbow Reading Notes\\videos\\rainbow\\rl-video-episode-0.mp4\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "                                                              \r"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Moviepy - Done !\n",
      "Moviepy - video ready C:\\Users\\18213\\Source\\Rainbow Reading Notes\\videos\\rainbow\\rl-video-episode-0.mp4\n",
      "score:  200.0\n"
     ]
    }
   ],
   "source": [
    "video_folder=\"videos/rainbow\"\n",
    "agent.test(video_folder=video_folder)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Render"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2023-09-06T05:42:58.039890100Z",
     "start_time": "2023-09-06T05:42:58.013534700Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/plain": "<IPython.core.display.HTML object>",
      "text/html": "\n        <video width=\"320\" height=\"240\" alt=\"test\" controls>\n        <source src=\"data:video/mp4;base64,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AAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAABQAAAAABAAACAAAAAAEAAAAAAAAAAQAAAQAAAAABAAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAABQAAAAABAAACAAAAAAEAAAAAAAAAAQAAAQAAAAABAAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAABQAAAAABAAACAAAAAAEAAAAAAAAAAQAAAQAAAAABAAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAABQAAAAABAAACAAAAAAEAAAAAAAAAAQAAAQAAAAABAAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAABQAAAAABAAACAAAAAAEAAAAAAAAAAQAAAQAAAAABAAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAABQAAAAABAAACAAAAAAEAAAAAAAAAAQAAAQAAAAABAAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAABQAAAAABAAACAAAAAAEAAAAAAAAAAQAAAQAAAAABAAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAABQAAAAABAAACAAAAAAEAAAAAAAAAAQAAAQAAAAABAAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAABQAAAAABAAACAAAAAAEAAAAAAAAAAQAAAQAAAAABAAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAABQAAAAABAAACAAAAAAEAAAAAAAAAAQAAAQAAAAABAAAFAAAAAAEAAAIAAAAAAQAAAAAAAAABAAABAAAAAAEAAAUAAAAAAQAAAgAAAAABAAAAAAAAAAEAAAEAAAAAAQAAAgAAAAAcc3RzYwAAAAAAAAABAAAAAQAAAMoAAAABAAADPHN0c3oAAAAAAAAAAAAAAMoAAARmAAAAVAAAACYAAAAXAAAAIQAAAEgAAAAlAAAAFAAAAB8AAABsAAAAHwAAABIAAAAdAAAAMgAAACgAAAAjAAAAHwAAAEEAAAAnAAAAIQAAACEAAAAbAAAAFAAAABIAAAASAAAAGwAAADYAAAAgAAAAIQAAABsAAAApAAAAHQAAAB8AAAAbAAAAIwAAAB0AAAAfAAAAGwAAACIAAAAbAAAAGwAAACUAAAAnAAAAHQAAACEAAAAaAAAAIgAAABsAAAAbAAAAQQAAACYAAAAkAAAAIQAAADwAAAAnAAAAIAAAAB4AAAAbAAAAJwAAAB4AAAAgAAAAGwAAACQAAAAeAAAAIAAAABsAAAAUAAAAEgAAABIAAAAbAAAAFAAAABIAAAASAAAAGwAAABQAAAASAAAAEgAAABsAAAAUAAAAEgAAABIAAAAbAAAAFAAAABIAAAASAAAAHgAAABQAAAASAAAAEgAAAFMAAAAiAAAAEgAAAB8AAAAbAAAAPQAAACIAAAAjAAAANwAAACkAAAAhAAAAHwAAAB4AAAAhAAAAGwAAABwAAAAbAAAAMwAAAB4AAAAfAAAAGwAAACQAAAAeAAAALwAAABsAAAA2AAAAHgAAACAAAAAbAAAAFAAAABIAAAASAAAAGgAAADYAAAAeAAAAIAAAABsAAAArAAAAGwAAABwAAAA4AAAALQAAAB4AAAAjAAAARAAAACYAAAAjAAAAGwAAABsAAAAnAAAAHgAAAB8AAAAbAAAAFAAAABIAAAASAAAAIgAAABQAAAASAAAAEgAAABsAAAAUAAAAEgAAABIAAAAaAAAAKwAAAB4AAAAfAAAAGwAAACoAAAAbAAAAGwAAAEoAAAAuAAAAHgAAACQAAAAfAAAAKAAAACEAAAAnAAAAGwAAACoAAAAgAAAAHwAAADoAAAAhAAAAHwAAABIAAAAbAAAAFAAAABIAAAASAAAAZgAAACIAAAASAAAAHwAAABsAAAAjAAAAHgAAAEAAAAA6AAAAOgAAACAAAAAgAAAAGgAAACYAAAAfAAAAIAAAABsAAAAzAAAAGwAAABwAAAAaAAAAFHN0Y28AAAAAAAAAAQAAADAAAABidWR0YQAAAFptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAC1pbHN0AAAAJal0b28AAAAdZGF0YQAAAAEAAAAATGF2ZjU4LjI5LjEwMA==\" type=\"video/mp4\"/>\n        </video>\n        "
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Played: videos/rainbow\\rl-video-episode-0.mp4\n"
     ]
    }
   ],
   "source": [
    "import base64\n",
    "import glob\n",
    "import io\n",
    "import os\n",
    "\n",
    "from IPython.display import HTML, display\n",
    "\n",
    "\n",
    "def ipython_show_video(path: str) -> None:\n",
    "    \"\"\"Show a video at `path` within IPython Notebook.\"\"\"\n",
    "    if not os.path.isfile(path):\n",
    "        raise NameError(\"Cannot access: {}\".format(path))\n",
    "\n",
    "    video = io.open(path, \"r+b\").read()\n",
    "    encoded = base64.b64encode(video)\n",
    "\n",
    "    display(HTML(\n",
    "        data=\"\"\"\n",
    "        <video width=\"320\" height=\"240\" alt=\"test\" controls>\n",
    "        <source src=\"data:video/mp4;base64,{0}\" type=\"video/mp4\"/>\n",
    "        </video>\n",
    "        \"\"\".format(encoded.decode(\"ascii\"))\n",
    "    ))\n",
    "\n",
    "\n",
    "def show_latest_video(video_folder: str) -> str:\n",
    "    \"\"\"Show the most recently recorded video from video folder.\"\"\"\n",
    "    list_of_files = glob.glob(os.path.join(video_folder, \"*.mp4\"))\n",
    "    latest_file = max(list_of_files, key=os.path.getctime)\n",
    "    ipython_show_video(latest_file)\n",
    "    return latest_file\n",
    "\n",
    "\n",
    "latest_file = show_latest_video(video_folder=video_folder)\n",
    "print(\"Played:\", latest_file)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "name": "python3",
   "language": "python",
   "display_name": "Python 3 (ipykernel)"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
